
import window from 'global/window'
import * as Dom from '../utils/dom'
import * as Events from '../utils/events'
import * as Obj from '../utils/obj'
import EventTarget from '../event-target'


/**
 * 返回一个对象是否已经被evented mixin applied
 * @param {*} object
 */
const isEvented = object => {
  return (
    object instanceof EventTarget ||
    !!object.eventBusEl_ &&
    ['on', 'one', 'off', 'trigger'].every(k => typeof object[k] === 'function')
  )
}

/**
 * 一个值是否是合法的event type: 不能是空字符串或者空数组
 * @param {*} type
 */
const isValidEventType = type =>
  (typeof type === 'string' && /\S/.test(type)) ||
  (Array.isArray(type) && !!type.length)

/**
 * 判断一个值是否是合法的event target，不是就抛错
 * @param {*} target
 */
const validateTarget = target => {
  if (!target.nodeName && !isEvented(target)) {
    throw new Error('非法的target, 必须是一个dom或者evented后的对象')
  }
}

/**
 * 判断一个值是否是合法的event type, 不是就抛错
 * @param {p} type
 */
const validateEventType = type => {
  if (!isValidEventType(type)) {
    throw new Error('非法的event type, 必须是非空的数组或字符串')
  }
}

const validateListener = (listener) => {
  if (typeof listener !== 'function') {
    throw new Error('Invalid listener; must be a function.');
  }
};

/**
 * 接受一个给on()或者one()的arguments数组，验证他们，并且正常化他们进一个对象
 * @param {*} self
 * @param {*} args
 */
const normalizeListenArgs = (self, args) => {
  // 如果arguments小于3个，那么target久是evented 后的object自己
  const isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_
  let target
  let type
  let listener

  if (isTargetingSelf) {
    target = self.eventBusEl_

    if (args.length >= 3) {
      args.shift()
    }

    [type, listener] = args
  } else {
    [target, type, listener] = args
  }

  validateTarget(target)
  validateEventType(type)
  validateListener(listener)

  listener = Fn.bind(self, listener)
  return {isTargetingSelf, target, type, listener}
}

/**
 * 给target的type事件添加一个监听器
 * @param {*} target
 * @param {*} method
 * @param {*} type
 * @param {*} listener
 */
const listen = (target, method, type, listener) => {
  validateTarget(target)

  if (target.nodeName) {
    Events[method](target, type, listener)
  } else {
    target[method](type, listener)
  }
}

/**
 * @mixin EventedMixin
 */
const EventedMixin = {
  on (...args) {
    const { isTargetingSelf, target, type, listener } = normalizeListenArgs(this, args)

    listen(target, 'on', type, listener)

    if (!isTargetingSelf) {
      const removeListenerOnDispose = () => this.off(target, type, listener)
      removeListenerOnDispose.guid = listener.guid
      const removeRemoverOnTargetDispose = () => this.off('dispose', removeListenerOnDispose)
      removeRemoverOnTargetDispose.guid = listener.guid
      listen(this, 'on', 'dispose', removeListenerOnDispose)
      listen(target, 'on', 'dispose', removeRemoverOnTargetDispose)
    }
  },

  one (...args) {
    const { isTargetingSelf, target, type, listener } = normalizeListenArgs(this, args)

    if (isTargetingSelf) {
      listen(target, 'one', type, listener)
    } else {
      const wrapper = (...largs) => {
        this.off(target, type, wrapper)
        listener.apply(null, largs)
      }

      wrapper.guid = listener.guid
      listen(target, 'one', type, wrapper)
    }
  },

  off (targetOrType, typeOrListener, listener) {
    if (!targetOrType || isValidEventType(targetOrType)) {
      Events.off(this.eventBusEl_, targetOrType, typeOrListener)
    } else {
      const target = targetOrType
      const type = typeOrListener

      validateTarget(target)
      validateEventType(type)
      validateListener(listener)

      listener = Fn.bind(this, listener)

      this.off('dispose', listener)

      if (target.nodeName) {
        Events.off(target, type, listener)
        Events.off(target, 'dispose', listener)
      } else if (isEvented(target)) {
        target.off(type, listener)
        target.off('dispose', listener)
      }
    }
  },

  trigger (event, hash) {
    return Events.trigger(this.eventBusEl_, event, hash)
  }
}


function evented (target, options = {}) {
  const {eventBusKey} = options
  if (eventBusKey) {
    if (!target[eventBusKey].nodeName) {
      throw new Error(`the eventBusKey "${eventBusKey}" 并不指向任何元素`)
    }
    target.eventBusEl_ = target[eventBusKey]
  } else {
    target.eventBusEl_ = Dom.createEl('span', { className: 'vjs-event-bus'})
  }
  Obj.assign(target, EventedMixin)

  target.on('dispose', () => {
    target.off()
    window.setTimeout(() => {
      target.eventBusEl_ = null
    }, 0)
  })
  return target
}

export default evented
export {isEvented}
